package main

import (
	"bytes"
	"crypto/ecdsa"
	"encoding/hex"
	"errors"
	"fmt"
	"os"

	"github.com/boltdb/bolt"
)

const dbFile = "blockchain.db"
const blocksBucket = "blocks"
const tipID = "l"
const genesisCoinbaseData = "神说要有光"

// Blockchain 区块链
type Blockchain struct {
	tip []byte
	db  *bolt.DB
}

// MineBlock 挖矿
func (bc *Blockchain) MineBlock(transaction []*Transaction) {

	var lastHash []byte
	err := bc.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		lastHash = b.Get([]byte(tipID))
		return nil
	})
	if err != nil {
		fmt.Println("failed to get lash block hash")
	}

	newBlock := NewBlock(transaction, lastHash) // 创建新的区块，可能比较耗时

	err = bc.db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		err := b.Put(newBlock.Hash, newBlock.Serialize())
		if err != nil {
			fmt.Println("failed to put new block into db")
		}
		err = b.Put([]byte(tipID), newBlock.Hash)
		if err != nil {
			fmt.Println("failed to update tip in db")
		}
		bc.tip = newBlock.Hash
		return nil
	})
}

// FindTransaction 根据ID查找交易记录
func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) {
	bci := bc.Iterator()
	for {
		block := bci.Next()
		for _, tx := range block.Transactions {
			if bytes.Compare(tx.ID, ID) == 0 {
				return *tx, nil
			}
		}
		if len(block.PrevBlockHash) == 0 {
			// 创世
			break
		}
	}
	return Transaction{}, errors.New("not found")
}

// FindUnspentTransactions  查找所有未花掉的交易记录
func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []Transaction {

	var unspentTXs []Transaction
	spentTXOs := make(map[string][]int) // make(map[string][]int) 创建一个map，key为string类型，值为[]int(int数组)
	bci := bc.Iterator()

	for {
		block := bci.Next()
		for _, tx := range block.Transactions {
			txID := hex.EncodeToString(tx.ID)

		Outputs:
			for outIdx, out := range tx.Vout {
				if spentTXOs[txID] != nil {
					for _, spentOut := range spentTXOs[txID] {
						if spentOut == outIdx {
							continue Outputs
						}
					}
				}

				if out.IsLockedWithKey(pubKeyHash) {
					unspentTXs = append(unspentTXs, *tx)
				}
			}

			if tx.IsCoinbase() == false {
				for _, in := range tx.Vin {
					if in.UsesKey(pubKeyHash) {
						inTxID := hex.EncodeToString(in.Txid)
						spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
					}
				}
			}
		}
		if len(block.PrevBlockHash) == 0 {
			// 创世区块儿，整个链已经遍历完成
			break
		}
	}
	return unspentTXs
}

// FindSpendableOutputs 查找可以花的钱， 也就是未花的比特币
func (bc *Blockchain) FindSpendableOutputs(pubKeyHash []byte, amount int) (int, map[string][]int) {

	unspentOutputs := make(map[string][]int)
	unspentTXs := bc.FindUnspentTransactions(pubKeyHash)
	accumulated := 0

Work:
	for _, tx := range unspentTXs {
		txID := hex.EncodeToString(tx.ID)
		for outIdx, out := range tx.Vout {
			if out.IsLockedWithKey(pubKeyHash) && accumulated < amount {
				accumulated += out.Value
				unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)
				if accumulated >= amount {
					break Work
				}
			}
		}
	}

	return accumulated, unspentOutputs
}

// FindUTXO 查找所有Output
func (bc *Blockchain) FindUTXO(pubKeyHash []byte) []TXOutput {
	var UTXOs []TXOutput
	unspentTransactions := bc.FindUnspentTransactions(pubKeyHash)

	for _, tx := range unspentTransactions {
		for _, out := range tx.Vout {
			if out.IsLockedWithKey(pubKeyHash) {
				UTXOs = append(UTXOs, out)
			}
		}
	}
	return UTXOs
}

// SignTransaction 为交易中的Input签名
func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) {
	prevTXs := make(map[string]Transaction)
	for _, vin := range tx.Vin {
		prevTX, err := bc.FindTransaction(vin.Txid)
		if err != nil {
			fmt.Println(err)
		}
		prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
	}
	tx.Sign(privKey, prevTXs)
}

// VerifyTransaction 验证Input签名
func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {
	prevTXs := make(map[string]Transaction)
	for _, vin := range tx.Vin {
		prevTX, err := bc.FindTransaction(vin.Txid)
		if err != nil {
			fmt.Println(err)
		}
		prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
	}
	return tx.Verify(prevTXs)
}

// NewBlockchain 使用创世区块创建新的区块链
func NewBlockchain(address string) *Blockchain {

	if !dbExists() {
		fmt.Println("no blockchain existed , create one first")
		os.Exit(1)
	}

	var tip []byte
	db, err := bolt.Open(dbFile, 0600, nil)
	if err != nil {
		fmt.Println("failed to open the db file")
	}
	err = db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		tip = b.Get([]byte(tipID))
		return nil
	})
	if err != nil {
		fmt.Println("failed to get tip")
	}
	bc := Blockchain{tip, db}
	return &bc
}

// CreateBlockchain 创建一个新的区块链
func CreateBlockchain(address string) *Blockchain {

	if dbExists() {
		fmt.Println("Blockchain already exists")
		os.Exit(1)
	}

	var tip []byte

	cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
	genesis := NewGenesisBlock(cbtx)

	db, err := bolt.Open(dbFile, 0600, nil)
	if err != nil {
		fmt.Println("failed to open the db file")
	}

	err = db.Update(func(tx *bolt.Tx) error {
		// 创建区块表(桶)
		b, err := tx.CreateBucket([]byte(blocksBucket))
		if err != nil {
			fmt.Println(err)
		}
		// 保存区块
		err = b.Put(genesis.Hash, genesis.Serialize())
		if err != nil {
			fmt.Println(err)
		}
		// 保存最新区块的Hash
		err = b.Put([]byte(tipID), genesis.Hash)
		if err != nil {
			fmt.Println(err)
		}
		tip = genesis.Hash

		return nil
	})

	if err != nil {
		fmt.Println(err)
	}

	bc := Blockchain{tip, db}

	return &bc
}

// dbExists 数据库是否存在
func dbExists() bool {
	if _, err := os.Stat(dbFile); os.IsNotExist(err) {
		return false
	}
	return true
}

// Iterator 创建迭代器
func (bc *Blockchain) Iterator() *BlockchainIterator {
	bci := &BlockchainIterator{
		bc.tip,
		bc.db,
	}
	return bci
}
